feat(memory): episode lifecycle — dedup-on-write + eviction (cap/TTL)#17
Merged
Conversation
EpisodeStore had no dedup and no eviction: episodes accumulated forever and near-duplicate sessions piled up unbounded (disk growth, noisier recall). This adds bounded, de-duplicated episode storage, config-gated with safe defaults, no on-disk format change. - Dedup-on-write: a new episode whose cosine similarity to an existing one is >= episode_dedup_threshold (default 0.92) REPLACES it (newest-wins). Uses an ephemeral RP embedder (the NewRPRanker primitive) over full on-disk summaries — never the shared dirty-rebuild vector index, avoiding mid-write re-entrancy. Provenance-gated: an untrusted near-dup can never evict a trusted/approved episode (trustRank mirrors the recall filter). - Eviction: prune-on-write applies TTL (episode_ttl_days, default 0/off) then a count cap (max_episodes, default 500), deleting both the .md file and the index entry. Crash-safe order: files -> writeIndex -> markDirty. Also exposes EpisodeStore.Prune() for session-end/CLI use. - Locking: dedup + .md write + index update + prune + markDirty now happen under a single e.mu hold (new writeLocked). Fixes a latent bug where re-writing the same sessionID appended a duplicate index entry. - Config: EpisodeDedupThreshold / MaxEpisodes / EpisodeTTLDays wired through MemoryConfig, DefaultMemoryConfig, both overlay sites, and a new NewEpisodeStoreWithLifecycle (bare NewEpisodeStore keeps lifecycle off, so existing callers/tests are unaffected). Documented in docs/CONFIG.md. Fact supersession is deferred (facts lack per-entry metadata; already covered by merge-on-write + session-end LLM consolidation). Tests: dedup replace/threshold/disabled, provenance safety (both directions), eviction by count + TTL, TTL-disabled, self-overwrite regression, evicted-id absent from recall, -race concurrency (16 goroutines), config defaults + overlay-to-store wiring. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adversarial review (AI Verification Protocol) found a path-traversal defense-in-depth gap: eviction/dedup called os.Remove on sessionIDs read straight from index.json without validation, so a crafted/corrupted index entry (e.g. "../victim") could delete a .md file OUTSIDE the episodes dir. Every other file op in the package (Read/Write/Promote) already validates. Add removeEpisodeFile(sessionID) which calls session.ValidateSessionID before os.Remove, and route all three eviction/dedup deletions through it. Adds a traversal-safe regression test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Contributor
Author
🔍 AI Verification Protocol v5.2.7 — CertificateClassification: Adversarial pass (Agent D) — findings & fixes
Probes that passed (no fix needed):
Axes
Signals
Verdict: |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
EpisodeStorehad no dedup and no eviction — episodes accumulated forever and near-duplicate sessions piled up unbounded (disk growth, noisier recall). This is the first slice of memory lifecycle (#6).Change
Bounded, de-duplicated episode storage. Config-gated, safe defaults, no on-disk format change.
Dedup-on-write (newest-wins, provenance-gated)
A new episode whose cosine similarity to an existing one ≥
episode_dedup_threshold(default 0.92) replaces it. Uses an ephemeral RP embedder (the existingNewRPRankerprimitive) over full on-disk summaries — deliberately not the shared dirty-rebuild vector index, avoiding a synchronous mid-write rebuild/re-entrancy. Provenance-safe: an untrusted near-dup can never evict a trusted/approved episode (trustRankmirrors the recall filter).Eviction (prune-on-write)
Applies TTL (
episode_ttl_days, default 0/off) then a count cap (max_episodes, default 500), deleting both the.mdfile and the index entry. Crash-safe order: files →writeIndex→markDirty(a crash leaves at most a dangling index entry, which rebuild/recall tolerate). Also exposesEpisodeStore.Prune()for session-end/CLI use.Locking + a latent-bug fix
Dedup +
.mdwrite + index update + prune +markDirtynow run under a singlee.muhold (writeLocked). This also fixes a latent bug where re-writing the samesessionIDappended a duplicate index entry.Config
EpisodeDedupThreshold/MaxEpisodes/EpisodeTTLDayswired throughMemoryConfig,DefaultMemoryConfig, both overlay sites, and a newNewEpisodeStoreWithLifecycle. The bareNewEpisodeStorekeeps lifecycle off, so existing callers/tests are unaffected. Documented indocs/CONFIG.md.Deferred: fact supersession — facts have no per-entry metadata (true supersession needs a format change) and already get semantic dedup via merge-on-write + session-end LLM consolidation.
Tests
Dedup replace/threshold/disabled, provenance safety (both directions), eviction by count + TTL, TTL-disabled, self-overwrite regression, evicted-id absent from recall,
-raceconcurrency (16 goroutines), config defaults + overlay-to-store wiring.Verification
🤖 Generated with Claude Code